package org.andengine.entity.modifier;

import org.andengine.entity.IEntity;
import org.andengine.util.adt.array.ArrayUtils;
import org.andengine.util.math.MathUtils;
import org.andengine.util.modifier.ease.EaseLinear;
import org.andengine.util.modifier.ease.IEaseFunction;

/**
 * (c) Zynga 2012
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @see {@link http://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline}
 * @see {@link http://algorithmist.wordpress.com/2009/10/06/cardinal-splines-part-4/}
 * @since 11:47:24 - 20.03.2012
 */
public class CardinalSplineMoveModifier extends DurationEntityModifier {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final CardinalSplineMoveModifierConfig mCardinalSplineMoveModifierConfig;

    private final IEaseFunction mEaseFunction;

    private final int mControlSegmentCount;
    private final float mControlSegmentCountInverse;

    // ===========================================================
    // Constructors
    // ===========================================================

    public CardinalSplineMoveModifier(final float pDuration, final CardinalSplineMoveModifierConfig pCardinalSplineMoveModifierConfig) {
        this(pDuration, pCardinalSplineMoveModifierConfig, null, EaseLinear.getInstance());
    }

    public CardinalSplineMoveModifier(final float pDuration, final CardinalSplineMoveModifierConfig pCardinalSplineMoveModifierConfig, final IEaseFunction pEaseFunction) {
        this(pDuration, pCardinalSplineMoveModifierConfig, null, pEaseFunction);
    }

    public CardinalSplineMoveModifier(final float pDuration, final CardinalSplineMoveModifierConfig pCardinalSplineMoveModifierConfig, final IEntityModifierListener pEntityModifierListener) {
        this(pDuration, pCardinalSplineMoveModifierConfig, pEntityModifierListener, EaseLinear.getInstance());
    }

    public CardinalSplineMoveModifier(final float pDuration, final CardinalSplineMoveModifierConfig pCardinalSplineMoveModifierConfig, final IEntityModifierListener pEntityModifierListener, final IEaseFunction pEaseFunction) {
        super(pDuration, pEntityModifierListener);

        this.mCardinalSplineMoveModifierConfig = pCardinalSplineMoveModifierConfig;
        this.mEaseFunction = pEaseFunction;

        this.mControlSegmentCount = pCardinalSplineMoveModifierConfig.getControlPointCount() - 1;
        this.mControlSegmentCountInverse = 1.0f / this.mControlSegmentCount;
    }

    @Override
    public CardinalSplineMoveModifier deepCopy() {
        return new CardinalSplineMoveModifier(this.mDuration, this.mCardinalSplineMoveModifierConfig.deepCopy(), this.mEaseFunction);
    }

    public CardinalSplineMoveModifier reverse() {
        return new CardinalSplineMoveModifier(this.mDuration, this.mCardinalSplineMoveModifierConfig.deepCopyReverse(), this.mEaseFunction);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    protected void onManagedInitialize(final IEntity pEntity) {

    }

    @Override
    protected void onManagedUpdate(final float pSecondsElapsed, final IEntity pEntity) {
        final float percentageDone = this.mEaseFunction.getPercentage(this.getSecondsElapsed(), this.mDuration);

		/* Calculate active control point. */
        final int p;
        if (percentageDone == 1) {
            p = this.mControlSegmentCount;
        } else {
            p = (int) (percentageDone / this.mControlSegmentCountInverse);
        }

		/* Retrieve control points. */
        final int p0 = MathUtils.bringToBounds(0, this.mControlSegmentCount, p - 1);
        final float pX0 = this.mCardinalSplineMoveModifierConfig.mControlPointXs[p0];
        final float pY0 = this.mCardinalSplineMoveModifierConfig.mControlPointYs[p0];

        final int p1 = MathUtils.bringToBounds(0, this.mControlSegmentCount, p);
        final float pX1 = this.mCardinalSplineMoveModifierConfig.mControlPointXs[p1];
        final float pY1 = this.mCardinalSplineMoveModifierConfig.mControlPointYs[p1];

        final int p2 = MathUtils.bringToBounds(0, this.mControlSegmentCount, p + 1);
        final float pX2 = this.mCardinalSplineMoveModifierConfig.mControlPointXs[p2];
        final float pY2 = this.mCardinalSplineMoveModifierConfig.mControlPointYs[p2];

        final int p3 = MathUtils.bringToBounds(0, this.mControlSegmentCount, p + 2);
        final float pX3 = this.mCardinalSplineMoveModifierConfig.mControlPointXs[p3];
        final float pY3 = this.mCardinalSplineMoveModifierConfig.mControlPointYs[p3];

		/* Calculate new position. */
        final float t = (percentageDone - (p * this.mControlSegmentCountInverse)) / this.mControlSegmentCountInverse;
        final float tt = t * t;
        final float ttt = tt * t;


		/*
         * Formula: s * (-ttt + 2tt – t) * P1 + s * (-ttt + tt) * P2 + (2ttt – 3tt + 1) * P2 + s(ttt – 2tt + t) * P3 + (-2ttt + 3tt) * P3 + s * (ttt – tt) * P4
		 */
        final float s = (1 - this.mCardinalSplineMoveModifierConfig.mTension) / 2;

        final float b1 = s * ((-ttt + (2 * tt)) - t); // (s * (-ttt + 2tt – t)) * P1
        final float b2 = (s * (-ttt + tt)) + (((2 * ttt) - (3 * tt)) + 1); // (s * (-ttt + tt) + (2ttt – 3tt + 1)) * P2
        final float b3 = (s * ((ttt - (2 * tt)) + t)) + ((-2 * ttt) + (3 * tt)); // (s * (ttt – 2tt + t) + (-2ttt + 3tt)) * P3
        final float b4 = s * (ttt - tt); // (s * (ttt – tt)) * P4

        final float x = ((pX0 * b1) + (pX1 * b2) + (pX2 * b3) + (pX3 * b4));
        final float y = ((pY0 * b1) + (pY1 * b2) + (pY2 * b3) + (pY3 * b4));

        pEntity.setPosition(x, y);
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public static final float cardinalSplineX(final float pX0, final float pX1, final float pX2, final float pX3, final float pT, final float pTension) {
        final float t = pT;
        final float tt = t * t;
        final float ttt = tt * t;

        final float s = (1 - pTension) / 2;

        final float b1 = s * ((-ttt + (2 * tt)) - t); // (s * (-ttt + 2tt – t)) * P1
        final float b2 = (s * (-ttt + tt)) + (((2 * ttt) - (3 * tt)) + 1); // (s * (-ttt + tt) + (2ttt – 3tt + 1)) * P2
        final float b3 = (s * ((ttt - (2 * tt)) + t)) + ((-2 * ttt) + (3 * tt)); // (s * (ttt – 2tt + t) + (-2ttt + 3tt)) * P3
        final float b4 = s * (ttt - tt); // (s * (ttt – tt)) * P4

        return ((pX0 * b1) + (pX1 * b2) + (pX2 * b3) + (pX3 * b4));
    }

    public static final float cardinalSplineY(final float pY0, final float pY1, final float pY2, final float pY3, final float pT, final float pTension) {
        final float t = pT;
        final float tt = t * t;
        final float ttt = tt * t;

        final float s = (1 - pTension) / 2;

        final float b1 = s * ((-ttt + (2 * tt)) - t); // (s * (-ttt + 2tt – t)) * P1
        final float b2 = (s * (-ttt + tt)) + (((2 * ttt) - (3 * tt)) + 1); // (s * (-ttt + tt) + (2ttt – 3tt + 1)) * P2
        final float b3 = (s * ((ttt - (2 * tt)) + t)) + ((-2 * ttt) + (3 * tt)); // (s * (ttt – 2tt + t) + (-2ttt + 3tt)) * P3
        final float b4 = s * (ttt - tt); // (s * (ttt – tt)) * P4

        return ((pY0 * b1) + (pY1 * b2) + (pY2 * b3) + (pY3 * b4));
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static class CardinalSplineMoveModifierConfig {
        // ===========================================================
        // Constants
        // ===========================================================

        private static final int CONTROLPOINT_COUNT_MINIMUM = 4;

        // ===========================================================
        // Fields
        // ===========================================================

        /* package */ private final float[] mControlPointXs;
        /* package */ private final float[] mControlPointYs;

        /* package */ final float mTension;

        // ===========================================================
        // Constructors
        // ===========================================================

        /**
         * @param pControlPointCount
         * @param pTension           [-1, 1]
         */
        public CardinalSplineMoveModifierConfig(final int pControlPointCount, final float pTension) {
            if (pControlPointCount < CardinalSplineMoveModifierConfig.CONTROLPOINT_COUNT_MINIMUM) {
                throw new IllegalArgumentException("A " + CardinalSplineMoveModifierConfig.class.getSimpleName() + " needs at least " + CardinalSplineMoveModifierConfig.CONTROLPOINT_COUNT_MINIMUM + " control points.");
            }

            this.mTension = pTension;

            this.mControlPointXs = new float[pControlPointCount];
            this.mControlPointYs = new float[pControlPointCount];
        }

        public CardinalSplineMoveModifierConfig deepCopy() {
            final int controlPointCount = this.getControlPointCount();

            final CardinalSplineMoveModifierConfig copy = new CardinalSplineMoveModifierConfig(controlPointCount, this.mTension);

            System.arraycopy(this.mControlPointXs, 0, copy.mControlPointXs, 0, controlPointCount);
            System.arraycopy(this.mControlPointYs, 0, copy.mControlPointYs, 0, controlPointCount);

            return copy;
        }

        public CardinalSplineMoveModifierConfig deepCopyReverse() {
            final CardinalSplineMoveModifierConfig copy = this.deepCopy();

            ArrayUtils.reverse(copy.mControlPointXs);
            ArrayUtils.reverse(copy.mControlPointYs);

            return copy;
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public int getControlPointCount() {
            return this.mControlPointXs.length;
        }

        public void setControlPoint(final int pIndex, final float pX, final float pY) {
            this.mControlPointXs[pIndex] = pX;
            this.mControlPointYs[pIndex] = pY;
        }

        public float getControlPointX(final int pIndex) {
            return this.mControlPointXs[pIndex];
        }

        public float getControlPointY(final int pIndex) {
            return this.mControlPointYs[pIndex];
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}
